/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.severez.bluetoothcontroller.ble;

import java.util.List;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;

import com.severez.bluetoothcontroller.ble.BLEListeners.OnConnectedListener;
import com.severez.bluetoothcontroller.ble.BLEListeners.OnDataAvailableListener;
import com.severez.bluetoothcontroller.ble.BLEListeners.OnDisconnectedListener;
import com.severez.bluetoothcontroller.ble.BLEListeners.OnServiceDiscoveredListener;

/**
 * Service for managing connection and data communication with a GATT server
 * hosted on a given Bluetooth LE device.
 */
public class BluetoothLeClass {
    private final static String TAG = BluetoothLeClass.class.getSimpleName();

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;

    private String mBluetoothDeviceAddress;
    public BluetoothGatt mBluetoothGatt;

    private OnConnectedListener mOnConnectedListener;
    private OnDisconnectedListener mOnDisconnectedListener;
    private OnServiceDiscoveredListener mOnServiceDiscoveredListener;
    private OnDataAvailableListener mOnDataAvailableListener;
    private static Context mContext;

    private static class LazyHolder {
	private static final BluetoothLeClass INSTANCE = new BluetoothLeClass();
    }

    private BluetoothLeClass() {
    }

    public static final BluetoothLeClass getInstance(Context c) {
	mContext = c;
	return LazyHolder.INSTANCE;
    }

    public void setOnConnectListener(OnConnectedListener l) {
	mOnConnectedListener = l;
    }

    public void setOnDisconnectListener(OnDisconnectedListener l) {
	mOnDisconnectedListener = l;
    }

    public void setOnServiceDiscoverListener(OnServiceDiscoveredListener l) {
	mOnServiceDiscoveredListener = l;
    }

    public void setOnDataAvailableListener(OnDataAvailableListener l) {
	mOnDataAvailableListener = l;
    }

    // Implements callback methods for GATT events that the app cares about. For
    // example,
    // connection change and services discovered.
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
	@Override
	public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
	    if (newState == BluetoothProfile.STATE_CONNECTED) {
		Log.i(TAG, "Connected to GATT server.");
		if (mOnConnectedListener != null)
		    mOnConnectedListener.onConnected(gatt);
		// Attempts to discover services after successful connection.
		Log.i(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices());

	    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
		if (mOnDisconnectedListener != null)
		    mOnDisconnectedListener.onDisconnected(gatt);
		Log.i(TAG, "Disconnected from GATT server.");
	    }
	}

	@Override
	public void onServicesDiscovered(BluetoothGatt gatt, int status) {
	    Log.w(TAG, "onServicesDiscovered" + status);
	    if (status == BluetoothGatt.GATT_SUCCESS && mOnServiceDiscoveredListener != null) {
		mOnServiceDiscoveredListener.onServiceDiscovered(gatt);
	    } else {
		Log.w(TAG, "onServicesDiscovered received: " + status);
	    }
	}

	@Override
	public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
	    Log.w(TAG, "leclass  onCharacteristicRead" + status);
	    if (mOnDataAvailableListener != null)
		mOnDataAvailableListener.onCharacteristicRead(gatt, characteristic, status);
	}

	public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
	    Log.w(TAG, "leclass  onCharacteristicWrite---->" + characteristic.getValue()[0]);
	    if (mOnDataAvailableListener != null)
		mOnDataAvailableListener.onCharacteristicWritten(gatt, characteristic, status);
	};

	public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
	    Log.w(TAG, "leclass  onCharacteristicChanged" + characteristic.getUuid().toString());
	    if (mOnDataAvailableListener != null)
		mOnDataAvailableListener.onCharacteristicChanged(gatt, characteristic);
	}

    };

    /**
     * Initializes a reference to the local Bluetooth adapter.
     * 
     * @return Return true if the initialization is successful.
     */
    public boolean initialize() {
	// For API level 18 and above, get a reference to BluetoothAdapter
	// through
	// BluetoothManager.
	if (mBluetoothManager == null) {
	    mBluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
	    if (mBluetoothManager == null) {
		Log.e(TAG, "Unable to initialize BluetoothManager.");
		return false;
	    }
	}

	mBluetoothAdapter = mBluetoothManager.getAdapter();
	if (mBluetoothAdapter == null) {
	    Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
	    return false;
	}

	return true;
    }

    /**
     * Connects to the GATT server hosted on the Bluetooth LE device.
     * 
     * @param address
     *            The device address of the destination device.
     * 
     * @return Return true if the connection is initiated successfully. The
     *         connection result is reported asynchronously through the
     *         {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     *         callback.
     */
    public boolean connect(final String address) {
	if (mBluetoothAdapter == null || address == null) {
	    Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
	    return false;
	}

	// Previously connected device. Try to reconnect.
	if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) {
	    Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
	    if (mBluetoothGatt.connect()) {
		return true;
	    } else {
		return false;
	    }
	}

	final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
	if (device == null) {
	    Log.w(TAG, "Device not found.  Unable to connect.");
	    return false;
	}
	// We want to directly connect to the device, so we are setting the
	// autoConnect
	// parameter to false.
	mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
	Log.d(TAG, "Trying to create a new connection.");
	mBluetoothDeviceAddress = address;
	return true;
    }

    /**
     * Disconnects an existing connection or cancel a pending connection. The
     * disconnection result is reported asynchronously through the
     * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     * callback.
     */
    public void disconnect() {
	if (mBluetoothAdapter == null || mBluetoothGatt == null) {
	    Log.w(TAG, "BluetoothAdapter not initialized");
	    return;
	}
	Log.i(TAG, "BluetoothAdapter disconnect");
	mBluetoothGatt.disconnect();
    }

    /**
     * After using a given BLE device, the app must call this method to ensure
     * resources are released properly.
     */
    public void close() {
	if (mBluetoothGatt == null) {
	    return;
	}
	mBluetoothGatt.close();
	mBluetoothGatt = null;
    }

    /**
     * Request a read on a given {@code BluetoothGattCharacteristic}. The read
     * result is reported asynchronously through the
     * {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
     * callback.
     * 
     * @param characteristic
     *            The characteristic to read from.
     */
    public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
	if (mBluetoothAdapter == null || mBluetoothGatt == null) {
	    Log.w(TAG, "BluetoothAdapter not initialized");
	    return;
	}
	mBluetoothGatt.readCharacteristic(characteristic);
    }

    /**
     * Enables or disables notification on a give characteristic.
     * 
     * @param characteristic
     *            Characteristic to act on.
     * @param enabled
     *            If true, enable notification. False otherwise.
     */
    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
	if (mBluetoothAdapter == null || mBluetoothGatt == null) {
	    Log.w(TAG, "BluetoothAdapter not initialized");
	    return;
	}
	mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
    }

    public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
	Log.i(TAG, "mBluetoothGatt  writeCharacteristic");
	mBluetoothGatt.writeCharacteristic(characteristic);
    }

    public void discoverServices() {
	if (mBluetoothAdapter == null || mBluetoothGatt == null) {
	    Log.w(TAG, "BluetoothAdapter not initialized");
	    return;
	}
	mBluetoothGatt.discoverServices();
    }

    /**
     * Retrieves a list of supported GATT services on the connected device. This
     * should be invoked only after {@code BluetoothGatt#discoverServices()}
     * completes successfully.
     * 
     * @return A {@code List} of supported services.
     */
    public List<BluetoothGattService> getSupportedGattServices() {
	if (mBluetoothGatt == null)
	    return null;

	return mBluetoothGatt.getServices();
    }
}
